package Remote

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
	"io"
	"log"
	"net"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type Host struct {
	User           string
	IP             string
	Port           string
	Password       string
	Key            string
	MD5sum         map[string]string
	Dir            string
	ToReceiveFiles []string
	ToDeleteFiles  []string
	sftpClient     *sftp.Client
	sshClient      *ssh.Client
}

func (h *Host) Connect() {
	c, err := h.loginSSH()
	if err != nil {
		log.Fatal("loginSSH err:", err)
	}
	h.sshClient = c

	s, err := sftp.NewClient(h.sshClient)
	if err != nil {
		log.Fatal(err)
	}
	h.sftpClient = s
}

func (h *Host) Clear() {
	err := h.sftpClient.Close()
	if err != nil {
		log.Println("h.sftpClient.Close err", err)
	}
	err = h.sshClient.Close()
	if err != nil {
		log.Println("h.sshClient.Close err", err)
	}
}

func (h *Host) Auth() {
	c, err := h.loginSSH()
	if err != nil {
		fmt.Println("登录远程服务器失败:", err)
		return
	}
	defer func() {
		err := c.Close()
		if err != nil {
			log.Println("c.Close err", err)
		}
	}()

	fmt.Println("成功登录远程服务器")
}

func (h *Host) CheckHost() bool {
	_, err := net.DialTimeout("tcp", h.IP+h.Port, time.Duration(5)*time.Second)
	if err != nil {
		log.Println("connect to", h.IP, h.Port, "failed!")
		return false
	}
	return true
}

// 通过密钥认证登录远程服务器
func (h *Host) loginSSH() (*ssh.Client, error) {
	privateKey := []byte(h.Key)
	// 解析私钥
	signer, err := ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(h.Password))
	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	// 配置SSH客户端
	config := &ssh.ClientConfig{
		User: user,
		Auth: []ssh.AuthMethod{
			ssh.PublicKeys(signer),
		},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}

	// 建立SSH连接
	client, err := ssh.Dial("tcp", h.IP+h.Port, config)
	if err != nil {
		return nil, err
	}

	return client, nil
}

// 用API方式计算远程服务器文件的MD5值（速度慢，弃用了）
func md5Sum(client *sftp.Client, remoteFilePath string) (string, error) {
	file, err := client.Open(remoteFilePath)
	if err != nil {
		//log.Println("client.Open err:", err)
		return "", err
	}

	hash := md5.New()
	_, err = io.Copy(hash, file)
	if err != nil {
		//log.Println("io.Copy err",err)
		return "", err
	}

	sum := hash.Sum(nil)
	value := hex.EncodeToString(sum)
	//fmt.Printf("%s sum is %q\n",remoteFilePath,value)
	return value, nil
}

// 根据指定文件名到远程服务器上获取文件的MD5sum值（通过登录服务器执行shell命令方式获取）
func (h *Host) FileOfMD5sum(filename string) (string, error) {
	//  md5sum /roots 2>&1|awk '{if ( length($1)==32 ) print $1 ; else if ( $3 != "No" ) print "dir" ; else print "no" ; }'

	CMD := fmt.Sprintf("md5sum %s 2>&1|awk '{if ( length($1)==32 ) print $1 ; else if ( $3 != \"No\" ) print \"dir\" ; else print \"no\" ; }'", filename)
	//fmt.Println("Remote->Host.go->FileOfMD5sum->CMD:", CMD)
	s, err := h.exec(CMD)

	//去掉s字符串末尾的\n
	md5sum := strings.TrimSpace(s)
	//filename是存在的目录函数返回字符串是dir,是存在的文件函数返回字符串是MD5sum值，不存在函数返回的字符串是no
	return md5sum, err
}

// 获取远程服务器指定目录下所有文件的MD5sum值
func (h *Host) GetMD5sum() {
	remoteDir := h.Dir
	walker := h.sftpClient.Walk(remoteDir)
	for walker.Step() {
		if walker.Path() == h.Dir {
			continue
		}
		// 获取相对路径
		relPath, err := filepath.Rel(remoteDir, walker.Path())
		if err != nil {
			log.Println("filepath.Rel err", err)
			return
		}
		linuxPath := strings.ReplaceAll(relPath, `\`, "/")

		fileInfo := walker.Stat()

		if fileInfo.IsDir() {
			h.MD5sum[linuxPath] = "dir"
		} else {
			//sum, err := md5Sum(h.sftpClient, walker.Path())
			sum, err := h.FileOfMD5sum(walker.Path())
			if err != nil {
				log.Println("md5Sum err:", err)
			} else {
				h.MD5sum[linuxPath] = sum
			}
		}
	}
}

func (h *Host) GetMD5sumByLocalFiles(localFiles []string) {
	//fmt.Printf("Remote->Host.go->GetMD5sumByLocalFiles->LocalFiles:%v\n", localFiles)
	for _, f := range localFiles {
		path := filepath.Join(h.Dir, f)
		linuxPath := strings.ReplaceAll(path, `\`, "/")
		//sum, err := md5Sum(h.sftpClient, linuxPath)
		sum, err := h.FileOfMD5sum(linuxPath)
		//fmt.Printf("Remote->Host.go->GetMD5sumByLocalFiles->sum:%s,linuxPath->%s,error->%v\n", sum, linuxPath, err)

		if err != nil {
			log.Println("md5Sum err:", err)
		} else {
			h.MD5sum[f] = sum
		}
	}
}

func (h *Host) CompareMD5sum(localMD5sumData map[string]string) {
	MReceive := make(map[string]bool)
	MDelete := make(map[string]bool)

	// 拿远程服务器的MD5sum值对比本地文件的MD5sum
	for LPath, LMD5sum := range localMD5sumData {
		MD5sum, ok := h.MD5sum[LPath]
		//if LPath == "logx" {
		//	fmt.Printf("Remote->Host.go->CompareMD5sum->MD5sum:%s  LMD5sum:%v,LPath->%s,ok->%v\n", MD5sum, LMD5sum, LPath, ok)
		//}
		//fmt.Printf("Remote->Host.go->CompareMD5sum->MD5sum:%s  LMD5sum:%v,LPath->%s,ok->%v\n", MD5sum, LMD5sum, LPath, ok)
		if !ok {
			//fmt.Printf("LPath:%s  LMD5sum:%v\n",LPath,LMD5sum)
			MReceive[LPath] = true
		} else {
			if MD5sum != LMD5sum {
				MReceive[LPath] = true
				if MD5sum != "no" {
					MDelete[LPath] = true
				}
			}
		}
	}
	// 拿本地文件的MD5sum值对比远程服务器的MD5sum
	for path, MD5 := range h.MD5sum {
		localMD5, ok := localMD5sumData[path]

		if !ok {
			// 远程服务器上有这个文件，但本地没有，远程服务器上此文件要删除
			//h.ToDeleteFiles = append(h.ToDeleteFiles, path)
			MDelete[path] = true
		} else {
			// 远程服务器上文件的md5sum和本地的不一样，远程服务器上此文件要删除，并接收本地的。
			//fmt.Printf("MD5:-%s,localMD5:-%s\n", MD5, localMD5)
			if MD5 != localMD5 {
				MReceive[path] = true
				if MD5 != "no" {
					MDelete[path] = true
				}
			}
		}
		//fmt.Printf("【%s】->的ToReceiveFiles: %s\n", h.IP, h.ToReceiveFiles)
	}

	//收集要发送的文件
	for path, ok := range MReceive {
		if ok {
			h.ToReceiveFiles = append(h.ToReceiveFiles, path)
		}
	}

	//收集要删除的文件
	for path, ok := range MDelete {
		if ok {
			h.ToDeleteFiles = append(h.ToDeleteFiles, path)
		}
	}
	//fmt.Printf("Remote->Host.go->CompareMD5sum->【%s】->的ToReceiveFiles: %s\n", h.IP, h.ToReceiveFiles)
	//fmt.Printf("Remote->Host.go->CompareMD5sum->【%s】->的ToDeleteFiles: %s\n", h.IP, h.ToDeleteFiles)
	//fmt.Println("Remote->Host.go->CompareMD5sum->remote:", h.MD5sum[`logx/access`], "local:", localMD5sumData[`logx/access`])
}

func (h *Host) deleteFiles() {
	client := h.sftpClient
	files := h.ToDeleteFiles
	for _, f := range files {
		path := filepath.Join(h.Dir, f)
		linuxPath := strings.ReplaceAll(path, `\`, `/`)
		//fmt.Println("Delete file:", linuxPath)

		err := client.RemoveAll(linuxPath)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			fmt.Printf("Delete %s failed!\n", linuxPath)
			log.Println("client.Remove err:", err)
		}
	}
}

func (h *Host) scpFile(fileName string) error {
	// 如果远程服务器同步目录不存在，那么就创建它
	_, err := h.sftpClient.Stat(h.Dir)
	if err != nil && os.IsNotExist(err) {
		err := h.sftpClient.MkdirAll(h.Dir)
		if err != nil {
			log.Println("sClient.MkdirAll", err)
			return err
		}
	}

	// 本地文件要传送到远程服务器，需要合成远程服务器的文件名
	filePath := filepath.Join(h.Dir, filepath.Base(fileName))

	// 此程序在windows下执行时，合成filename的文件名路径是windows格式，要调整为Linux格式
	linuxPath := strings.ReplaceAll(filePath, `\`, `/`)

	remoteFile, err := h.sftpClient.Create(linuxPath)
	if err != nil {
		log.Println("sClient.Create err", err)
		return err
	}
	defer func() {
		err := remoteFile.Close()
		if err != nil {
			log.Println("remoteFile.Close err:", err)
		}
	}()

	// 打开本地文件
	localFile, err := os.Open(fileName)
	if err != nil {
		log.Println(`os.Open(localFilePath) err:`, err)
		return err
	}
	defer func() {
		err := localFile.Close()
		if err != nil {
			log.Println("localFile.Close err:", err)
		}
	}()

	// 将本地文件内容拷贝到远程文件
	_, err = io.Copy(remoteFile, localFile)
	if err != nil {
		log.Println(`io.Copy(remoteFile, localFile) err`, err)
		return err
	}
	return nil
}

func (h *Host) compress(LocalFolder string) (string, error) {

	// 合成压缩后的文件名，windows下执行，路径是windows格式，Linux下执行,路径是linux格式
	target := filepath.Join("./", h.IP+".tar.gz")

	// 创建目标文件
	file, err := os.Create(target)
	if err != nil {
		return "", fmt.Errorf("创建压缩文件失败：%v", err)
	}
	defer func() {
		err := file.Close()
		if err != nil {
			log.Println("file.Close err:", err)
		}
	}()

	// 创建gzip写入流
	gw := gzip.NewWriter(file)
	defer func() {
		err := gw.Close()
		if err != nil {
			log.Println("gw.Close err:", err)
		}
	}()

	// 创建tar写入流
	tw := tar.NewWriter(gw)
	defer func() {
		err := tw.Close()
		if err != nil {
			log.Println("tw.Close err:", err)
		}
	}()

	//fmt.Println("h.ToReceiveFiles:", h.ToReceiveFiles)

	for _, Path := range h.ToReceiveFiles {
		f := filepath.Join(LocalFolder, Path)
		fileInfo, err := os.Stat(f)
		if err != nil {
			log.Println("Path:", Path)
			log.Println("file:", f, "os.State err:", err)
			return "", err
		}
		header, err := tar.FileInfoHeader(fileInfo, "")
		if err != nil {
			log.Println("tar.FileInfoHeader err:", err)
			return "", err
		}

		//获取相对路径，windows下执行，路径是windows格式，Linux下执行是linux格式
		rel, err := filepath.Rel(LocalFolder, f)
		if err != nil {
			log.Printf("sourcePath --> %s,k--> %s,err --->%v\n"+
				"", LocalFolder, f, err)
			return "", err
		}

		// 生成的压缩文件最终要在Linux系统下解压，如果rel的路径是windows格式，那么必须转换成linux格式
		linuxPath := strings.ReplaceAll(rel, `\`, "/")

		header.Name = linuxPath

		// 设置解压后文件的权限
		if fileInfo.IsDir() {
			header.Mode = 0755
		} else {
			header.Mode = 0644
		}

		err = tw.WriteHeader(header)
		if err != nil {
			log.Println("tw.WriteHeader err", err)
			return "", err
		}

		if !fileInfo.IsDir() {
			file, err := os.Open(f)
			if err != nil {
				log.Println("f:", f, "os.Open err:", err)
				return "", err
			}

			_, err = io.Copy(tw, file)
			if err != nil {
				return "", err
			}
			err = file.Close()
			if err != nil {
				log.Println("file.Close err", err)
			}
		}
	}
	return target, nil
}

func (h *Host) exec(cmd string) (string, error) {
	session, err := h.sshClient.NewSession()
	if err != nil {
		log.Fatalf("无法创建会话：%v", err)
	}

	defer func() {
		err = session.Close()
		if err != nil && err.Error() != "EOF" {
			log.Println("session.Close err:", err)
		}
	}()

	// 执行系统命令
	var stdout, stderr bytes.Buffer
	session.Stdout = &stdout
	session.Stderr = &stderr
	err = session.Run(cmd)
	if err != nil {
		log.Printf("命令执行失败：%v\n%s", err, stderr.String())
		return "", err
	}

	// 输出命令执行结果
	//fmt.Println(stdout.String())

	return stdout.String(), nil
}

func (h *Host) Rsync(LocalFolder string) {
	if len(h.ToDeleteFiles) > 0 {
		//删除远程服务器上的多余文件和需要被更新的文件。
		fmt.Printf("\n【%s】服务器开始删除文件......\n", h.IP)
		h.deleteFiles()
	}

	if len(h.ToReceiveFiles) == 0 {
		return
	}
	//在本地把要传送的文件打包压缩
	filePath, err := h.compress(LocalFolder)
	if err != nil {
		log.Println("h.compress err", err)
		return
	}

	//传送本地压缩文件到远程服务器
	fmt.Printf("\n【%s】服务器开始上传文件......\n", h.IP)
	err = h.scpFile(filePath)
	if err != nil {
		log.Println("h.scpFile err", err)
		return
	}

	//删除本地的压缩文件
	err = os.Remove(filePath)
	if err != nil {
		log.Println("os.Remove err", err)
	}

	// 登录远程服务器执行命令：解压文件，再删除压缩文件。
	forDropFile := filepath.Base(filePath)
	cmd := fmt.Sprintf("cd %s && tar xf %s && rm -f %s", h.Dir, forDropFile, forDropFile)
	//fmt.Println("exec cmd :", cmd)

	_, err = h.exec(cmd)
	if err != nil {
		log.Println("h.exec err", err)
	}

	cmd = fmt.Sprintf("bash %s/run.sh", h.Dir)

	_, err = h.exec(cmd)
	if err != nil {
		log.Println("h.exec err", err)
	}
}
